Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operators to extend interactivity with python objects #12

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

ncguilbeault
Copy link
Contributor

@ncguilbeault ncguilbeault commented Mar 4, 2024

This PR is intended to add initial support for extended interactivity and manipulation of python objects outside of python scripting. Below are a few highlighted key features to demonstrate these new nodes.

Basic python primitives
Some nodes provide the ability to instantiate python primitives, such as PyInt, PyFloat, and PyString.
Screenshot from 2024-03-04 12-35-32

Import
It is possible to use the Import function to import packages directly into a module. For example, this would be the equivalent of using import [python-package] in a script.
Screenshot from 2024-03-04 12-33-08

Invoke
Invoke can be used to instantiate a callable object. It can work directly on python objects to instantiate them or it can be used to invoke an attribute of the object. It supports passing Args to the invocation.

InvokeMethod
InvokeMethod can be used to call a method of an object. Supports passing Args.

Args
Args will take a single python object or a tuple/list/array of python objects and convert it to a type of PyTuple which can be passed as Args to a function.

GetAttr
The GetAttr method can be used to access an attribute of a python object.

Example with numpy
Take the example of creating a numpy array. In python, you would do something like this.

import numpy as np
arr = np.array([0])

To achieve the equivalent python script, you can do the following. You can import the np library object to Invoke and set the Callable property to array, passing a PyInt as Args. Here is how that might look:
Screenshot from 2024-03-04 14-28-14

Then, if you wanted to use the transpose, you can pass the resulting numpy array object to InvokeMethod and specify the Method property to be transpose. Alternatively, instead of calling the transpose function of the numpy array, you could call GetAttr with Attribute set to T, which is the equivalent of performing a transpose using arr.T in python.

Copy link
Member

@glopesdev glopesdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left several comments, my overall feeling is that it might be easier if we turn each of these into its own PR, since there are design decisions with all of them which might require extended discussion.

For the LDS package, I think it actually might be more practical to just implement what you need directly as C# nodes in the package, to avoid breaking changes later.

[WorkflowElementCategory(ElementCategory.Transform)]
public class Args
{
public IObservable<Pythonnet.PyTuple> Process(IObservable<object> source)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would both be more idiomatic and performant here to have strongly-typed overloads for compatible types, or implement the operator using a full-blown ExpressionBuilder.

This way we wouldn't have to rely on dynamic type checking and reflection to know how to construct the PyTuple object, since we would know the exact types at compile time. We could then automatically generate code that builds the tuple for the exact type signature and skip all the expensive reflection calls altogether.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes required? They seem unrelated to the new operators, but I wonder whether you may have had trouble compiling in Linux perhaps?


public IObservable<PyObject> Process(IObservable<PyObject> source)
{
if (string.IsNullOrEmpty(Attribute))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are testing here the Attribute property it might be best to cache its value locally, otherwise if the property is externalized and changing dynamically, someone could set its value to null while the sequence is running and accidentally bypass this test.

{
if (string.IsNullOrEmpty(Attribute))
{
throw new Exception("Attribute cannot be null or empty.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The framework design guidelines recommend not throwing the Exception base type. Since properties are not function arguments, for bonsai operators we have converged on using InvalidOperationException as the convention for operator state validation errors.

{
if (!obj.HasAttr(Attribute))
{
throw new Exception($"PyObject does not have attribute {Attribute}.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check and throw here? I would expect that simply calling GetAttr would throw some kind of python exception if the attribute does not exist.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as GetItem and InvokeMethod re. making the PyObject be the property.

{
if (Value == null)
{
throw new Exception("Value cannot be null.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re. exception typing and caching.

public class SetItem
{
[Description("The index to set.")]
public int Index { get; set; } = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re. variable types of indexers. The current implementation seems to only support int, but we know string also works, and possibly many other types. Flipping the logic around and making PyObject the property would keep things more flexible.

{
if (!Pythonnet.PySequence.IsSequenceType(obj))
{
throw new Exception($"PyObject is not a type of sequence.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment regarding preventive testing, I would hope in all these cases that pythonnet would provide some kind of sane failure exception for incompatible types, especially given the dynamic nature of python, so we shouldn't have to pretest any objects.

}
if (Value == null)
{
throw new Exception("Value cannot be null.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, so you can't you set a value to None? Maybe pythonnet doesn't accept null directly, but wondering whether here we should in that case convert null to the None singleton?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants